package org.xx.armory.log4j2;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.CompositeFilterComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.FilterComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.apache.logging.log4j.status.StatusLogger;
import org.xx.armory.config.Configuration;
import org.xx.armory.config.data.RedisSection;
import org.xx.armory.config.log4j2.AppenderSection;
import org.xx.armory.config.log4j2.ConsoleAppenderSection;
import org.xx.armory.config.log4j2.Log4j2Section;
import org.xx.armory.config.log4j2.LoggerSection;
import org.xx.armory.config.log4j2.RedisAppenderSection;
import org.xx.armory.config.log4j2.RollingFileAppenderSection;
import org.xx.armory.config.log4j2.RootLoggerSection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static org.apache.commons.lang3.StringUtils.split;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.logging.log4j.Level.INFO;
import static org.apache.logging.log4j.core.Filter.Result.DENY;
import static org.apache.logging.log4j.core.Filter.Result.NEUTRAL;
import static org.apache.logging.log4j.core.config.Configurator.initialize;
import static org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory.newConfigurationBuilder;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 用于实现具体配置的配置器。
 * <p>引入此配置器的目的是避免{@link Log4j2Section}与{@literal log4j2}包之间的强依赖关系。</p>
 */
public final class Log4j2Configurator {
    private final Logger logger = StatusLogger.getLogger();

    private Configuration configuration;

    private Log4j2Configurator(
            Configuration configuration
    ) {
        if (configuration == null) {
            logger.fatal("argument \"configuration\" cannot be null");
        }

        this.configuration = configuration;
    }

    public static Log4j2Configurator newInstance(
            Configuration configuration
    ) {
        return new Log4j2Configurator(configuration);
    }

    private static LayoutComponentBuilder newLayout(
            ConfigurationBuilder<BuiltConfiguration> builder,
            AppenderSection section
    ) {
        return builder.newLayout("PatternLayout").addAttribute("pattern", section.getLayout());
    }

    private void configureAppender(
            ConfigurationBuilder<BuiltConfiguration> builder,
            AppenderSection section
    ) {
        notNull(builder, "builder");
        notNull(section, "section");

        final AppenderComponentBuilder appenderBuilder;
        if (section instanceof RollingFileAppenderSection) {
            final RollingFileAppenderSection rollingFileAppenderSection = (RollingFileAppenderSection) section;
            final ComponentBuilder triggeringPolicy = builder.newComponent("Policies")
                                                             .addComponent(builder.newComponent("TimeBasedTriggeringPolicy").addAttribute("interval", "1").addAttribute("modulate", true))
                                                             .addComponent(builder.newComponent("SizeBasedTriggeringPolicy").addAttribute("size", "100MB"));
            final ComponentBuilder rolloverStrategy = builder.newComponent("DefaultRolloverStrategy")
                                                             .addAttribute("fileIndex", "max")
                                                             .addAttribute("min", 1)
                                                             .addAttribute("max", "99");
            appenderBuilder = builder.newAppender(rollingFileAppenderSection.getName(), "RollingFile")
                                     .addAttribute("fileName", rollingFileAppenderSection.getFileName())
                                     .addAttribute("filePattern", rollingFileAppenderSection.getFilePattern())
                                     .addAttribute("append", true)
                                     .addAttribute("bufferedIO", true)
                                     .addAttribute("immediateFlush", true)
                                     .addComponent(triggeringPolicy)
                                     .addComponent(rolloverStrategy)
                                     .add(newLayout(builder, rollingFileAppenderSection));
        } else if (section instanceof ConsoleAppenderSection) {
            final ConsoleAppenderSection consoleAppenderSection = (ConsoleAppenderSection) section;
            appenderBuilder = builder.newAppender(consoleAppenderSection.getName(), "Console")
                                     .addAttribute("target", consoleAppenderSection.getTarget())
                                     .add(newLayout(builder, consoleAppenderSection));
        } else if (section instanceof RedisAppenderSection) {
            final RedisAppenderSection redisAppenderSection = (RedisAppenderSection) section;

            final String dataSourceName = redisAppenderSection.getDataSourceName();
            final RedisSection redisSection = this.configuration.getData().getRedis(dataSourceName);
            if (redisSection == null) {
                logger.fatal("cannot find redis({})", dataSourceName);
                return;
            }

            final ComponentBuilder redisProvider = builder.newComponent("Redis")
                                                          .addAttribute("uri", redisSection.getUri())
                                                          .addAttribute("timeout", redisSection.getTimeout())
                                                          .addAttribute("key", redisAppenderSection.getKey());
            appenderBuilder = builder.newAppender(redisAppenderSection.getName(), "NoSql").addComponent(redisProvider);

        } else {
            logger.warn("unknown appender: {}", section);
            return;
        }

        // 设置各项过滤器。

        List<FilterComponentBuilder> filterBuilders = new ArrayList<>();

        // 日志级别过滤器。
        final String level = trimToEmpty(section.getLevel());
        if (!level.isEmpty()) {
            final FilterComponentBuilder thresholdFilter = builder.newFilter("ThresholdFilter", NEUTRAL, DENY)
                                                                  .addAttribute("level", level);
            filterBuilders.add(thresholdFilter);
        }

        // Marker过滤器。
        final String marker = trimToEmpty(section.getMarker());
        if (!marker.isEmpty()) {
            final FilterComponentBuilder markerFilter = builder.newFilter("MarkerFilter", NEUTRAL, DENY)
                                                               .addAttribute("marker", marker);
            filterBuilders.add(markerFilter);
        }

        if (filterBuilders.size() == 1) {
            // 如果只存在一个过滤器，则直接添加此过滤器到Appender。
            appenderBuilder.add(filterBuilders.get(0));
        } else if (filterBuilders.size() > 1) {
            // 如果存在多个过滤器，那么首先创建CompositeFilter
            final ComponentBuilder<CompositeFilterComponentBuilder> compositeFilter = builder.newComponent("filters");
            // 将多个过滤器依次加入CompositeFilter
            for (final FilterComponentBuilder filter : filterBuilders) {
                compositeFilter.addComponent(filter);
            }
            // 最后添加CompositeFilter到Appender。
            appenderBuilder.addComponent(compositeFilter);
        } else {
            // 不存在任何过滤器，那么不执行任何添加。
        }

        // 添加Appender到构造器。
        builder.add(appenderBuilder);
    }

    private void configureRoot(
            ConfigurationBuilder<BuiltConfiguration> builder,
            RootLoggerSection section
    ) {
        notNull(builder, "builder");
        notNull(section, "section");

        final RootLoggerComponentBuilder rootBuilder = builder.newRootLogger(section.getLevel());

        for (final String appender : split(section.getAppenders(), ",")) {
            rootBuilder.add(builder.newAppenderRef(appender));
        }

        builder.add(rootBuilder);
    }

    private void configureLogger(
            ConfigurationBuilder<BuiltConfiguration> builder,
            LoggerSection section
    ) {
        notNull(builder, "builder");
        notNull(section, "section");

        final LoggerComponentBuilder loggerBuilder = builder.newLogger(section.getName(), section.getLevel());

        for (final String appender : split(section.getAppenders(), ",")) {
            loggerBuilder.add(builder.newAppenderRef(appender));
        }

        builder.add(loggerBuilder);
    }

    /**
     * 执行配置。
     */
    public final void configure() {
        final Collection<AppenderSection> appenderSections = configuration.getLog4j2().getAppenders();
        final RootLoggerSection rootLoggerSection = configuration.getLog4j2().getRoot();
        final Collection<LoggerSection> loggerSections = configuration.getLog4j2().getLoggers();

        // 以代码方式配置log4j2。
        logger.info("reconfigure log4j2");

        // 关闭当前的log4j2日志系统。
        LogManager.shutdown();

        // 创建配置器。
        final ConfigurationBuilder<BuiltConfiguration> builder = newConfigurationBuilder();
        builder.setStatusLevel(INFO);
        builder.setConfigurationName("armory.config");
        builder.setPackages(Log4j2Configurator.class.getPackage().getName());

        // 配置Appender
        for (final AppenderSection section : appenderSections) {
            if (section.isEnabled()) {
                configureAppender(builder, section);
            }
        }

        // 配置Logger
        for (final LoggerSection section : loggerSections) {
            configureLogger(builder, section);
        }

        // 配置Root
        configureRoot(builder, rootLoggerSection);

        // 重新初始化log4j2日志系统。
        initialize(builder.build());
    }
}
